package org.pc2r.game;

import java.net.Socket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Logger;

import org.pc2r.exception.PC2RException;
import org.pc2r.game.model.Player;
import org.pc2r.game.play.GameExecution;
import org.pc2r.game.play.RematchCallable;
import org.pc2r.game.prepare.Preparator;
import org.pc2r.network.SocketReadWriter;
import org.pc2r.protocole.EnumRequest;
import org.pc2r.protocole.message.EmptyMessage;
import org.pc2r.protocole.message.Message;
import org.pc2r.protocole.message.PlayersMessage;

/**
 * La classe de gestion du déroulement d'une partie. Cette classe s'occupe de
 * toute "l'intelligence" d'une {@link Game}
 * 
 */
public class GameManager implements Callable<List<Player>> {

	static final private long delay = 7000L;// 30L * 1000L;

	final private Game game;
	private Logger logger;
	private boolean isStarted;
	private int nbPlayers;
	private GameStore store;

	public GameManager(Logger logger, GameStore store) {
		assert logger != null;
		this.game = new Game();
		this.logger = logger;
		this.store = store;
	}

	public String addPlayer(String name, Socket socket) throws PC2RException {
		return addPlayer(name, new SocketReadWriter(socket));
	}

	public synchronized String addPlayer(String name, SocketReadWriter socket)
			throws PC2RException {
		String add = game.addPlayer(name, socket);
		nbPlayers++;
		if (nbPlayers == 2) {
			Timer timer = new Timer();
			timer.schedule(new TimerTask() {

				@Override
				public void run() {
					isStarted = true;
					// Lancement de la partie. On la lance dans cette thread
					// afin d'éviter
					// d'en créer une nouvelle.
					GameManager manager = GameManager.this;
					List<Player> call = manager.call();
					if(call == null) return;
					List<String> after = new ArrayList<>(call.size());
					for (Player player : call) {
						String name2 = player.getName();
						store.getOrCreate().addPlayer(name2, game.get(name2));
						after.add(name2);
					}
					game.closeExcept(after);
					store.deleteGameManager(manager);
				}
			}, delay);
		}
		logger.info("Le joueur " + add + " s'est connecté.");
		return add;
	}

	@Override
	public List<Player> call() {
		// On lance le broadcast et on corrige les erreurs. Si erreur, on coupe
		// la
		// partie.
		if (false == beforeStart()) {
			return null;
		}
		logger.info("Préparation de la partie en cours...");
		// On prépare la partie. On s'occupe de positionner tous les sous-marins
		if (false == new Preparator(this, logger).call()) {
			return null;
		}
		logger.info("Démarrage de la partie. Les joueurs sont dans l'ordre de priorité : "
				+ game.getSortedPlayers());
		Player winner = new GameExecution(logger, this).call();
		if (winner != null) {
			logger.info("Le gagnant de la partie est " + winner);
		}
		// On va lire les réponses de tous les clients non déconnectés
		List<Player> sortedPlayers = Collections.unmodifiableList(game
				.getSortedPlayers());
		ExecutorService exec = Executors.newFixedThreadPool(game.size());
		@SuppressWarnings("unchecked")
		// On lance dans size threads les lectures
		Future<Boolean>[] results = new Future[game.size()];
		try {
			int i = 0;
			for (Player play : sortedPlayers) {
				// On éxecute
				results[i] = exec.submit(new RematchCallable(game, play));
			}
		} finally {
			exec.shutdown();
		}
		int i = 0;
		List<Player> replay = new ArrayList<>();
		for (Player play : sortedPlayers) {
			try {
				Boolean result = results[i++].get(30L, TimeUnit.SECONDS);
				if (result == Boolean.TRUE) {
					// On ajoute le joueur dans ceux qui veulent rejouer
					replay.add(play);
				} else {
					// On supprime proprement le joueur s'il a dit bye
					game.remove(play.getName());
				}
			} catch (InterruptedException | ExecutionException
					| TimeoutException exception) {
			}
		}
		return replay;
	}

	public final boolean removeAndStop(String name) {
		boolean haveToStop = game.remove(name);
		// S'il y a moins de 2 joueurs, on arrête les frais
		if (haveToStop) {
			logger.warning("La partie est terminée par manque de joueurs.");
			// On envoie un message DRAWGAME au joueur restant
			close(new EmptyMessage(EnumRequest.DRAWGAME));
		}
		return haveToStop;
	}

	protected final boolean beforeStart() {
		List<String> players = game.players();
		logger.info("Le partie démarre... Les joueurs sont : " + players);
		List<String> broadcast = game.broadcast(new PlayersMessage(players));
		// Certains joueurs ont été déconnectés (volontairement ou pas).
		if (broadcast != null) {
			for (String delete : broadcast) {
				// On supprime tous les joueurs déconnectés et on stoppe la
				// partie
				// proprement
				if (removeAndStop(delete)) {
					// La partie n'estp as jouable (il y a minimum 1 joueur), on
					// la vire
					return false;
				}
			}
			// Sinon on continue la partie. Exemple Sur 4 joueurs 2 ont déco,
			// alors ça
			// sera une partie à 2 joueurs
		}
		return true;
	}

	public boolean isStarted() {
		return this.isStarted;
	}

	public boolean isFull() {
		return this.game.isFull() || isStarted();
	}

	public Game getGame() {
		return game;
	}

	public void close(Message message) {
		game.broadcast(message);
		game.close();
	}
}
